package camera;

import javax.media.opengl.GL2;
import javax.media.opengl.awt.GLJPanel;

import panel.control.HasProperties;
import util.Util;
import window.Axis;

public class Camera implements HasProperties {

	public static final int PROPERTY_EYE 		= 4;
	public static final int PROPERTY_TARGET 	= 5;
	public static final int PROPERTY_DISTANCE 	= 6;
	public static final int PROPERTY_PITCH		= 7;
	public static final int PROPERTY_YAW 		= 8;
	
	private GLJPanel canvas;
	private double [] eye;
	private double [] target;
	private double [] up;
	
	private double fov;
	private double close;
	private double far;
	
	public Camera( GLJPanel canvas ) {
		this.canvas = canvas;
		this.eye = new double[3];
		this.target = new double[3];
		this.up = new double[3];
		
		eye[0] = 3.0;
		eye[1] = 3.0;
		eye[2] = 3.0;
		
		target[0] = 0.0;
		target[1] = 0.0;
		target[2] = 0.0;
		
		up[0] = 0.0;
		up[1] = 0.0;
		up[2] = 1.0;
		
		this.close = 1;
		this.far = 1000;
		this.fov = 60.0;
	}
	
	/* ----------------------------------------------------
	 * Application functions
	 */
	
	public void apply( GL2 gl ) {
		setProjection(gl);
		setView(gl);
	}
	
	public void setProjection( GL2 gl ) {
		gl.glMatrixMode(GL2.GL_PROJECTION);
		gl.glLoadIdentity();
		Util.glu.gluPerspective(fov, canvas.getWidth()*1.0/canvas.getHeight(), close, far);
	}
	
	public void setView( GL2 gl ) {
		double yaw = Util.toRad(getYaw());
		double pitch = getPitch();
		double lx = -Math.cos(yaw);
		double ly = -Math.sin(yaw);
		double lz = 1.0;
		double f = (pitch > 0) ? (1) : (-1);
		
		gl.glMatrixMode(GL2.GL_MODELVIEW);
		gl.glLoadIdentity();
		Util.glu.gluLookAt( eye[Axis.X], eye[Axis.Y], eye[Axis.Z],
				   target[Axis.X], target[Axis.Y], target[Axis.Z],
				   f*lx, f*ly, lz );
	}
	
	/* ----------------------------------------------------
	 * Manipulation functions
	 * 
	 * NOTE: distance, yaw and pitch are considered relative
	 * properties, always obtained from eye/target positions.
	 * 
	 * Use setEyePositionFromRelative(...) to set eye
	 * positions based on relative properties.
	 */
	
	public boolean setEyePosition( double p, int axis ) {
		return eye[axis] != (eye[axis] = p);
	}
	
	public boolean setEyePosition( double x, double y, double z ) {
		boolean hasChange = setEyePosition(x, Axis.X);
		hasChange = setEyePosition(y, Axis.Y) || hasChange;
		hasChange = setEyePosition(z, Axis.Z) || hasChange;
		return hasChange;
	}
	
	public boolean setEyePositionFromRelative( double distance, double yaw, double pitch ) {
		pitch = Util.toRad(Math.min(Math.max(pitch, -90.0), 90.0));
		yaw = Util.toRad(yaw);
		return setEyePosition( target[Axis.X] + distance*Math.cos(pitch)*Math.cos(yaw),
								target[Axis.Y] + distance*Math.cos(pitch)*Math.sin(yaw),
								target[Axis.Z] + distance*Math.sin(pitch) );
	}
	
	public boolean moveEye( double d, int axis ) {
		return setEyePosition( eye[axis] + d, axis );
	}
	
	public boolean moveEye( double dx, double dy, double dz ) {
		return setEyePosition( eye[Axis.X] + dx,
								eye[Axis.Y] + dy,
								eye[Axis.Z] + dz );
	}
	
	public boolean setTargetPosition( double p, int axis ) {
		return target[axis] != (target[axis] = p);
	}
	
	public boolean setTargetPosition( double x, double y, double z ) {
		boolean hasChange = setTargetPosition(x, Axis.X);
		hasChange = setTargetPosition(y, Axis.Y) || hasChange;
		hasChange = setTargetPosition(z, Axis.Z) || hasChange;
		return hasChange;
	}
	
	public boolean moveTarget( double d, int axis ) {
		return setTargetPosition( target[axis] + d, axis );
	}
	
	public boolean moveTarget( double dx, double dy, double dz ) {
		return setTargetPosition( target[Axis.X] + dx,
									target[Axis.Y] + dy,
									target[Axis.Z] + dz );
	}
	
	public boolean setDistance( double d ) {
		return setEyePositionFromRelative( d, getYaw(), getPitch() );
	}
	
	public boolean zoom( double d ) {
		return setDistance( getDistance() + d );
	}
	
	public boolean setYaw( double yaw ) {
		return setEyePositionFromRelative( getDistance(), yaw, getPitch() );
	}
	
	public boolean rotateYaw( double a ) {
		return setYaw( getYaw() + a );
	}
	
	public boolean setPitch( double pitch ) {
		return setEyePositionFromRelative( getDistance(), getYaw(), pitch );
	}
	
	public boolean rotatePitch( double a ) {
		return setYaw( getPitch() + a );
	}
	
	/* ----------------------------------------------------
	 * Getter functions
	 */
	
	public double getDistance() {
		double dx = eye[Axis.X] - target[Axis.X];
		double dy = eye[Axis.Y] - target[Axis.Y];
		double dz = eye[Axis.Z] - target[Axis.Z];
		
		return Math.sqrt(dx*dx + dy*dy + dz*dz);
	}
	
	public double getYaw() {
		double dx = eye[Axis.X] - target[Axis.X];
		double dy = eye[Axis.Y] - target[Axis.Y];
		
		return Util.toDeg(Math.atan2(dy, dx));
	}
	
	public double getPitch() {
		double dx = eye[Axis.X] - target[Axis.X];
		double dy = eye[Axis.Y] - target[Axis.Y];
		double dp = Math.sqrt(dx*dx + dy*dy);
		double dz = eye[Axis.Z] - target[Axis.Z];
		
		return Util.toDeg(Math.atan2(dz, dp));
	}
	
	public double getEyePosition( int axis ) {
		return eye[axis];
	}
	
	public double getTargetPosition( int axis ) {
		return target[axis];
	}
	
	public double getFieldOfView() {
		return fov;
	}
	
	public double getCloseDistance() {
		return close;
	}
	
	public double getFarDistance() {
		return far;
	}
	
	/* ----------------------------------------------------
	 * General property functions
	 * (from HasProperties interface)
	 */
	
	public double getProperty( int index, int property ) {
		switch (property) {
			case PROPERTY_EYE: 		return eye[index];
			case PROPERTY_TARGET: 	return target[index];
			case PROPERTY_DISTANCE:	return getDistance();
			case PROPERTY_YAW: 		return getYaw();
			case PROPERTY_PITCH: 	return getPitch();
		}
		
		return 0.0;
	}
	
	public boolean setProperty( double value, int index, int property ) {
		switch (property) {
			case PROPERTY_EYE:		return setEyePosition(value, index);
			case PROPERTY_TARGET:	return setTargetPosition(value, index);
			case PROPERTY_DISTANCE:	return setDistance(value);
			case PROPERTY_YAW:		return setYaw(value);
			case PROPERTY_PITCH:	return setPitch(value);
		}
		
		return false;
	}
	
	public boolean changeProperty( double value, int index, int property ) {
		return setProperty( getProperty( index, property ) + value, index, property );
	}
}
